// MyVpn.cpp : Defines the entry point for the console application.
//

#include "SysLib.h"

#ifdef OS_WIN

#include "Common.h"
#include "TunDev.h"
#include "Config.h"
#include "Tunnel.h"
#include "Protocal.h"


PVpnConfig config;
PUserInfo userConf;

PTunnel tunnel = NULL;
PSocketWrapper sock = NULL;
PTunDev tun = NULL;
PBuffer readBuf = NULL;
PBuffer writeBuf = NULL;

void do_exit() {
    log_info("do_exit()");
    exit(1);
}

void init_client() {
    if (!tun) {
        tun = tundev_alloc(config->dev);
        if (!tun) {
            log_error("tundev_alloc() error");
            do_exit();
        }
    }
    tunnel = tunnel_new(config->read_buffer_size, config->write_buffer_size);
    tunnel->username = string_alloc(userConf->username);
    readBuf = tunnel->read_buffer;
    writeBuf = tunnel->write_buffer; 
}


int connect_client() {
    PTunnel oldTunnel = tunnel;
    init_client();
    tunnel->client_auth_seed = oldTunnel->auth_seed;
    tunnel_delete(oldTunnel);
    
    sock = tunnel->socket = socket_connect(config->host, config->port);
    if (!tunnel->socket) {
        log_error("connect() error: %d", GetLastError());
        return -1;
    }
//    int flag = 1;
//    if (-1 == setsockopt(sfd, IPPROTO_TCP, TCP_NODELAY, (char *)&flag, sizeof(flag))) {
//        log_error("Error setting tcp no delay");
//        return -1;
//    }
//    if (fcntl(sfd, F_SETFL, fcntl(sfd, F_GETFL) | O_NONBLOCK) < 0) {
//        log_error("Error setting the socket in nonblocking mode");
//        return -1;
//    }
    log_info("Connected to server %s:%d", config->host, config->port);
    
    if (client_auth(tunnel, config) >= 0) {
        tunnel->last_from_net = tunnel->last_to_net = time(0);
        return 0;
    }
    return -1;
}


int main(int argc, const char* argv[]) {
    set_log_file(NULL);
    /**/
    if (argc <= 1) {
        printf("Run with myvpn <config_file>");
        return 1;
    }
    {
        WSADATA wsaData;
        WORD wVersionRequested = MAKEWORD( 2, 0 );
        if (WSAStartup(wVersionRequested, &wsaData)) {
            log_error("Error init winsock");
            return 1;
        }
    }
    config = read_config(argv[1]); /**/
    //config = read_config("d:\\MyVpnTray\\bin\\config.txt");
    if (!config) {
        log_error("Error reading config file");
        return 1;
    }
    if (config->mode != CLIENT) {
        log_error("Only client(0) mode support for windows vpn client");
        return 1;
    }
    if (!vpn_config_verify(config)) {
        log_error("Error verify config, exit");
        return 1;
    }
    userConf = config->users[0];
    set_log_level(config->log_level);
    set_log_file(config->log_file);
    
    log_info("Finish parsing parameters");

    init_client();
   
    char *tunReadBuffer = (char *) malloc(sizeof(char) * 2000);
    char *tunWriteBuffer = (char *) malloc(sizeof(char) * 2000);
    char *heartbeatBuffer = (char *) malloc(sizeof(char) * 2000);
    HANDLE hEvents[4] = {0};
    int hTypes[4] = {0};
    int hCount = 0;
    bool error = true;
    bool auth = false;
    DWORD error_no;
    while(1) {
        if (error) {
            auth = false;
            tunnel_close(tunnel);
            log_info("Connecting to vpn server");
            if (connect_client() != 0) {
                log_error("Connect error, sleep 3s and retry");
                Sleep(3000);
                continue;
            }
            hCount = 0;
            if (tun->reading) {
                hCount = 1;
                hEvents[0] = tun->olRead->hEvent;
                hTypes[0] = 1;
            }
            error = false;
            log_info("Connected to vpn server");
        }
        
        if (!sock->reading) {
            DWORD nread;
            bool ret = ReadFile((HANDLE)sock->fd, buffer_pwrite(readBuf), buffer_writable(readBuf), &nread, sock->olRead);
            if ((!ret) && ((error_no = GetLastError()) != ERROR_IO_PENDING)) {
                error = true;
                log_error("Async read net error: %d", error_no);
                continue;
            }
            sock->reading = true;
            hEvents[hCount] = sock->olRead->hEvent;
            hTypes[hCount++] = 0;
        }
        if (!tun->reading) {
            DWORD nread;
            bool ret = ReadFile(tun->handle, tunReadBuffer, 2000, &nread, tun->olRead);
            if (!ret) {
                if ((error_no = GetLastError()) != ERROR_IO_PENDING) {
                    error = true;
                    log_error("Async read tun error: %d", error_no);
                    continue;
                }
            }
            tun->reading = true;
            hEvents[hCount] = tun->olRead->hEvent;
            hTypes[hCount++] = 1;
        }

        if (buffer_readable(writeBuf) && !sock->writing) {
            DWORD nwrite;
            bool ret = WriteFile((HANDLE)sock->fd, buffer_pread(writeBuf), buffer_readable(writeBuf), &nwrite, sock->olWrite);
            if (!ret) {
                if ((error_no = GetLastError()) != ERROR_IO_PENDING) {
                    error = true;
                    log_error("Async write net error: %d", error_no);
                    continue;
                }
            }
            sock->writing = true;
            hEvents[hCount] = sock->olWrite->hEvent;
            hTypes[hCount++] = 2;
        }

        DWORD dwWaitRet = WaitForMultipleObjects(hCount, hEvents, FALSE, 5000); //Wait multiple IO requests
        log_trace("Wait return %d", dwWaitRet);
        switch(dwWaitRet - WAIT_OBJECT_0)  
        {  
        case 0:
        case 1:
        case 2:
            switch (hTypes[dwWaitRet - WAIT_OBJECT_0]) {
            case 0:
                log_trace("Read from net");
                sock->reading = false;
                if (sock->olRead->Internal != ERROR_SUCCESS) {
                    error = true;
                    log_error("Async read net notify error: %d", sock->olRead->Internal);
                    continue;
                }
                tunnel->last_from_net = time(0);
                if (sock->olRead->InternalHigh == 0) {
                    error = true;
                    log_error("Async read net get length 0, socket closed");
                    continue;
                }
                buffer_pwrite_inc(readBuf, sock->olRead->InternalHigh);
                if (auth) {
                    int plen;
                    for (;;) {
                        plen = decode_data(readBuf, tunWriteBuffer);
                        if (plen == 0) {
                            log_debug("Receive heartbeat");
                        } else if (plen == -1) {
                            buffer_compact(readBuf);
                            break;
                        } else if (plen < -1) {
                            log_error("Process net recv data error");
                            error = true;
                            break;
                        } else {
                            tundev_write(tun, tunWriteBuffer, plen);
                        }
                    }
                } else {
                    int auth_ret = client_auth(tunnel, config);
                    char tmp[256];
                    if (auth_ret == 1) {
                        bool quick_reconnect = tun->local_ip && (strcmp(tun->local_ip, tunnel->client_ip) == 0);
                        if (quick_reconnect) {
                            log_info("Hot reconnect success");
                        } else if (tun->local_ip) {
                            log_info("Cold reconnect detected, exit for restart");
                            do_exit();
                        } else {
                            log_info("Vpn connect success");
                            log_info("Auth pass, local ip: %s, remote ip: %s", tunnel->client_ip, tunnel->server_ip);

                            if (tundev_ifconfig(tun, tunnel->client_ip, tunnel->server_ip) != 0) {
                                log_error("ifconfig error");
                                do_exit();
                            }

                            PRoute *push_routes = read_routes_cache(config->route_cache_file);
                            if (push_routes) {
                                PRoute *route;
                                for (route=push_routes; *route; route++) {
                                    tundev_route(tun, route[0]->address, route[0]->prefix);
                                }
                            }
                            if (config->routes) {
                                PRoute *route;
                                for (route=config->routes; *route; route++) {
                                    tundev_route(tun, route[0]->address, route[0]->prefix);
                                }
                            }
                            if (userConf->script) {
                                sprintf(tmp, "%s %s %s %s %s", userConf->script, config->dev, tunnel->client_ip, tunnel->server_ip, tun->ifnum);
                                log_info("Running: %s", tmp);
                                int script_ret = system(tmp);
                                if (script_ret) {
                                    log_error("Script return error: %d", script_ret);
                                }
                            }
                        }
                        sprintf(tmp, "echo Connected: %s > status", tunnel->client_ip);
                        system(tmp);
                        auth = true;
                    } else if (auth_ret < 0) {
                        error = true;
                        log_error("Auth error");
                    }
                }
                break;
            case 1:
                log_trace("Read from tun");
                tun->reading = false;
                if (tun->olRead->Internal != ERROR_SUCCESS) {
                    error = true;
                    log_error("Async read tun notify error: %d", tun->olRead->Internal);
                    continue;
                } 
                if (tun->olRead->InternalHigh == 0) {
                    error = true;
                    log_error("Async read tun get length 0, tun closed");
                    continue;
                }
                if (auth && -1 > encode_data(tunReadBuffer, tun->olRead->InternalHigh, writeBuf)) {
                    error = true;
                    continue;
                }
                break;
            case 2:
                log_trace("Write to net");
                if (sock->olWrite->Internal != ERROR_SUCCESS) {
                    error = true;
                    log_error("Async write net notify error: %d", sock->olWrite->Internal);
                    continue;
                }
                tunnel->last_to_net = time(0);
                sock->writing = false;
                buffer_pread_inc(writeBuf, sock->olWrite->InternalHigh);
                buffer_compact(writeBuf);
                break;
            }
            hCount--;
            int i;
            for (i=dwWaitRet - WAIT_OBJECT_0; i<hCount; i++) {
                hEvents[i] = hEvents[i + 1];
                hTypes[i] = hTypes[i + 1];
            }
            break;
        case WAIT_TIMEOUT: {
            time_t now = time(0);
            if (now - tunnel->last_to_net > 15) {
                log_debug("Sending heartbeat");
                encode_data(heartbeatBuffer, 0, writeBuf);
            }
            if (now - tunnel->last_from_net > 60) {
                log_error("Heartbeat timeout");
                error = true;
            }
            break;
        }    
        case WAIT_FAILED:
            log_error("Wait event error: %d", GetLastError());
            error = true;
            break;
        default:
            log_error("Wait event return other: %d", dwWaitRet);
            error = true;
            break;
        }

    }
    return(0);
}


#endif
